import QtQuick 2.0
import QtQuick.Controls 2.15
import QtQml.Models 2.15
import FileIO 1.0
import "../view"

Item {
    id: clientInstaller

    signal predownloadFinished()
    signal installError()
    signal installFinished()
    signal installCanceled()
    signal installSelectLanguagesCanceled()

    readonly property bool isActive: installerState != stateIdle && installerState != stateDone

    property string progressText: ""
    property int progressPercent: 0

    property int installerState: stateIdle
    readonly property int stateIdle: 0
    readonly property int stateDownloading: 1
    readonly property int stateRevertingWinePatch: 2
    readonly property int stateUnarchiving: 3
    readonly property int stateApplyingWinePatch: 4
    readonly property int stateReapplyingWinePatch: 5
    readonly property int stateDone: 6

    property string installingVersion
    property var currentFile
    property var filesToDownload: []
    property var downloadedFiles: []
    property var installedFiles: []

    readonly property string voicePackPath: "/GenshinImpact_Data/StreamingAssets/Audio/GeneratedSoundBanks/Windows/"
    readonly property string persistentDir: "/GenshinImpact_Data/Persistent/"
    readonly property string audioLangPath: persistentDir + "audio_lang_14"
    readonly property var langMap: ({
      "zh-cn": "Chinese",
      "en-us": "English(US)",
      "ja-jp": "Japanese",
      "ko-kr": "Korean"
    })
    readonly property string preDownloadVersionFile: downloader.downloadDir + 'pre-download.version'

    property var checkedLanguages: ({})


    Downloader {
        id: downloader
        downloadDir: `${settings.gamePath}/../_update_gi_download/`
        multiConnections: settings.multiConnections
        limitDownload: settings.limitDownload
        limitDownloadValue: settings.limitDownloadValue

        onProgressChanged: {
            let downloadingCount = downloadedFiles.length + 1
            let totalCount = filesToDownload.length + downloadedFiles.length + 1
            const na = qsTr("N/A")
            clientInstaller.progressText = downloadingCount + " " + qsTr("of") + " " + totalCount
                + ", " + qsTr("Downloaded: ") + (progress.downloaded || na) + "/" + (progress.total || na)
                + ", " + qsTr("Speed: ") + (progress.speed ? (progress.speed + "/" + qsTr("s")) : na)
                + ", " + qsTr("Estimated: ") + (progress.estimated || na)
            progressPercent = progress.percent || 0
        }

        onFinished: {
            downloadedFiles.push(currentFile)
            downloadNextFile()
        }

        onError: {
            if (!canceled) {
                ui.log(qsTr("Failed to download file") + errors)
                handleError()
            }
        }
    }

    WinePatcher {
        id: winePatcher
        gameVersion: availableVersion || localVersionLoader.version
        gamePath: settings.gamePath
        anchors.fill: parent

        onPatchReverted: {
            if (installerState === stateReapplyingWinePatch) {
                applyWinePatch()
            } else {
                installerState = stateUnarchiving
                installNextArchive()
            }
        }
        onErrorRevertingPatch: handleError()

        onPatchApplied: {
            installerState = stateDone
            installFinished()
        }
        onErrorApplyingPatch: handleError()
        onPatchCanceled: handleCancel()
    }

    ArchiveInstaller {
        id: archiveInstaller
        outputDir: settings.gamePath

        onProgressChanged: {
            let installingCount = installedFiles.length + 1
            let totalCount = installedFiles.length + downloadedFiles.length + 1

            const na = qsTr("N/A")
            let progressText = installingCount + " " + qsTr("of") + " " + totalCount
                + ", " + progress.status + ": " + currentFile.file
            if (progress.total) {
                progressText += ", " + qsTr("Unpacked: ") + (progress.unarchived || na) + "/" + progress.total
            }

            clientInstaller.progressText = progressText
            progressPercent = progress.percent || 0
        }

        onFinished: {
            installedFiles.push(currentFile)
            installNextArchive()
        }
        onError: handleError()
    }


    Dialog {
        id: dialogLanguages
        anchors.centerIn: parent
        title: window.title
        standardButtons: Dialog.Ok | Dialog.Cancel
        modal: true
        focus: true
        width: block * 6
        height: block * 3 + block / 2 * listLanguages.count

        ListView {
            id: listLanguages

            property int count: 0

            interactive: false
            anchors.fill: parent
            model: ListModel {
            }
            delegate: Button {
                checkBox: true
                checkable: true
                checked: checkedLanguages[model.language] || false
                title: model.name
                width: listLanguages.width
                height: block / 2
                border.width: 0
                color: "transparent"
                horizontalAlignment: Text.AlignLeft

                onClicked: {
                    checkedLanguages[model.language] = checked
                }
            }
        }

        function show() {
            listLanguages.model.clear()
            checkedLanguages = {}

            let audioLangs = readAudioLangs()

            var languages = []
            let voicePacks = webVersionLoader.json.data.game.latest.voice_packs
            for (let pack of voicePacks) {
                checkedLanguages[pack.language] = audioLangs.indexOf(langMap[pack.language]) !== -1
                languages.push(pack.language)
                listLanguages.model.append({
                    language: pack.language,
                    name: langMap[pack.language] || pack.language
                })
            }
            listLanguages.count = languages.length
            open()
        }

        function readAudioLangs() {
            let audioLangs
            let audioLangsText = FileIO.readTextFile(settings.gamePath + audioLangPath)
            if (audioLangsText) {
                audioLangs = audioLangsText.split("\n")
            } else {
                audioLangs = []
                for (let lang in langMap) {
                    audioLangs.push(langMap[lang])
                }
            }
            return audioLangs
        }

        function writeAudioLangs() {
            let audioLangsText = ""
            for (let lang in langMap) {
                if (checkedLanguages[lang]) {
                    audioLangsText += langMap[lang] + "\n"
                }
            }

            FileIO.createDirectory(settings.gamePath + persistentDir)
            FileIO.writeTextFile(settings.gamePath + audioLangPath, audioLangsText)
        }

        onAccepted: {
            writeAudioLangs()
            performInstallOrUpdate()
        }
        onRejected: installSelectLanguagesCanceled()
    }


    function handleError() {
        clientInstaller.installerState = clientInstaller.stateIdle
        installError()
    }

    function handleCancel() {
        clientInstaller.installerState = clientInstaller.stateIdle
        installCanceled()
    }


    function start() {
        if (ui.uiState == ui.uiStateInstalling) {
            dialogLanguages.show()
        } else {
            performInstallOrUpdate()
        }
    }

    function performInstallOrUpdate() {
        let isInstalling = ui.uiState == ui.uiStateInstalling
        let gamePath = settings.gamePath
        let data = webVersionLoader.json.data
        let game = isPreDownloading ? data.pre_download_game : data.game
        let availableVersion = game.latest.version

        if (availableVersion === installedVersion) {
            // shall never happen
            ui.log(qsTr("Already up to date"))
            ui.uiState = ui.uiStateInstall
            return
        }

        installingVersion = availableVersion

        ui.log("")

        let diffToInstall = null

        if (installedVersion) {
            for (let diff of game.diffs) {
                if (diff.version === installedVersion) {
                    diffToInstall = diff
                    break
                }
            }
            if (!diffToInstall) {
                ui.log(qsTr("No compatible patch found, performing full install"))
            }
        }

        // TODO check for available free disk space

        if (diffToInstall) {
            ui.log((isPreDownloading ? qsTr("Pre-downloading client update") : qsTr("Updating client"))
                    + ` ${installedVersion} => ${availableVersion}...`)
        } else {
            ui.log((isPreDownloading ? qsTr("Pre-downloading client version") : qsTr("Installing client version"))
                    + ` ${availableVersion}...`)
        }

        var files = []
        var itemToFile = (item) => {
            let fileName = item.path.replace(/.*\/([^\/]+)(\?.*)?$/, "$1")
            return { lang: item.language, url: item.path, md5: item.md5, file: fileName }
        }

        let nodeToInstall = diffToInstall || game.latest
        files.push(itemToFile(nodeToInstall))
        for (let voicePack of nodeToInstall.voice_packs) {
            let installPack = isInstalling
                    ? checkedLanguages[voicePack.language]
                    : FileIO.isFileExists(gamePath + voicePackPath + langMap[voicePack.language])
            if (installPack) {
                files.push(itemToFile(voicePack))
            }
        }

        filesToDownload = files
        downloadedFiles = []
        currentFile = null
        installedFiles = []

        FileIO.createDirectory(gamePath)

        installerState = stateDownloading
        downloadNextFile()
    }

    function getPreDownloadedVersion() {
        return FileIO.isFileExists(preDownloadVersionFile) ? FileIO.readTextFile(preDownloadVersionFile) : null
    }

    function downloadNextFile() {
        currentFile = filesToDownload.shift()
        if (!currentFile) {
            if (!isPreDownloading) {
                revertWinePatch()
            } else {
                installerState = stateDone
                ui.log(qsTr("Pre-download was finished successfully"))
                FileIO.writeTextFile(preDownloadVersionFile, preDownloadVersion)
                predownloadFinished()
            }

            return
        }

        downloader.download(currentFile.url, currentFile.file, currentFile.md5)
    }

    function reapplyWinePatch() {
        installerState = stateReapplyingWinePatch
        winePatcher.revertPatch()
    }

    function revertWinePatch() {
        installerState = stateRevertingWinePatch
        winePatcher.revertPatch()
    }

    function installNextArchive() {
        currentFile = downloadedFiles.shift()
        if (!currentFile) {
            FileIO.removeDirectory(downloader.downloadDir)
            localVersionLoader.version = installingVersion
            localVersionLoader.save()

            // do not start applyWinePatch() here
            // it's intended that user clicks "Patch" button
            installerState = stateDone
            installFinished()
            return
        }

        ui.log(qsTr("Installing") + " " + currentFile.file + "...")

        archiveInstaller.install(downloader.downloadDir + currentFile.file)
    }

    function applyWinePatch() {
        installerState = stateApplyingWinePatch
        winePatcher.applyPatch()
    }

    function isPatchInstalled() {
        return winePatcher.isPatchInstalled()
    }

    function cancel() {
        if (downloader.isRunning) {
            downloader.cancel()
        }
        if (archiveInstaller.isRunning) {
            archiveInstaller.cancel()
        }
        installerState = stateIdle
    }

}
